--[[
    redis -> mysql

--]]


-- 数据类型
local DT = {
    Integer = 1,
    String = 2,
    Float = 3,
    Double = 4, -- 与Float 一样 
}


local __ST = "__SET_TIME__"

local function get_expire_time()
    return g.const.redis_time.default_exprie
end

local function error_args(desc, ...)
    error(string.format("%s args=%s", desc, tostring({...})))
end

local function quote(v)
    if type(v) == "string" then
        return "'" .. v .. "'" --return mysql.quote_sql_str(v)
    else
        return v
    end
end

local function dict2kvlist(d)
    local lst = {}
    if not d then return lst end
    for k, v in pairs(d) do
        table.insert(lst, k)
        table.insert(lst, v)
    end
    return lst
end

local function execute_sql(sql)
    if not sql then return end
    local res = g.callMysql(sql)
    if not res then
        error(string.format("execute sql failed, return nil sql=%s", sql))
    end
    if res.badresult == true then
        error(string.format("execute sql failed, res=%s sql=%s", tostring(res), sql))
    end
    return res
end

local function need_re_expire(st, ex, now, tablename)
    --if no_need_re_expire_tables[tablename] then return end
    now = now or g.time()
    local re_ex_ttl = g.const.redis_time.re_expire_ttl
    if (ex - (now - st)) < re_ex_ttl then
        return true
    end
    return false
end

local futil = {}
function futil.get_specific_fields(t, fields)
    if not (t and fields) then return end

    local ret = {}
    for _, f in pairs(fields) do
        ret[f] = t[f]
    end

    return ret
end

local function _try_number(v)
    local tmp = tonumber(v)
    if tmp then
        return tmp
    else
        return v
    end
end
----------------------------------------------------
---
local M = {DT = DT} 

-- 创建一个表映射 
-- |table_name|: 表名
-- |fields|: 列, 以列名为键的kv lua table. 格式为
--      {ColumnName1 = {Datatype = DT.Integer, Default = xx},
--       ColumnName2 = {Datatype = DT.String, Default = xx},}
-- |first_key_name|, |second_key_name|, 对应 dataworker.lua 里的
-- load_single/load_multi的参数. |second_key_name|为nil 表示是单行.
-- |update_fname|, |load_fname|, |delete_fname| 分别表示对数据进行
-- 更新,加载,删除时调用 datawork.lua 的接口名.
--
-- dataworker.lua 里的接口参数格式为
--     update_fname(tablename, fkn, fk, skn, rkey, records)
--     load_fname(tablename, fkn, fk, skn, rkey, fields)
--     delete_fname(tablename, fkn, fk, skn, sk, rkey, fields)
--
--     其中fkn(first key name), fk(first key), skn(second key name),
--     sk(second key)
function M.new(table_name, fields, first_key_name, second_key_name, 
               update_fname, load_fname, delete_fname, encode, decode)
    assert(table_name and fields and first_key_name)
    local o = {}
    o.table_name       = table_name -- 表名
    o.fields           = fields -- 列
    o.first_key_name   = first_key_name -- 主键
    o.second_key_name  = second_key_name -- 二级索引
    o.encode           = encode -- 保存时对记录进行编码的函数
    o.decode           = decode -- 加载时对记录进行解码的函数
    o.field_names = {}
    for k, _ in pairs(o.fields) do
        if k ~= first_key_name and k ~= second_key_name then
            table.insert(o.field_names, k)
        end
    end
    if update_fname and load_fname and delete_fname then
        o.update_fname     = M[update_fname] -- 更新函数名
        o.load_fname       = M[load_fname] -- 加载函数名
        o.delete_fname     = M[delete_fname] -- 删除函数名
    else
        o.update_fname     = M.update_single
        o.load_fname       = M.load_single
        o.delete_fname     = M.delete_single
    end


    o.record = nil
    o.records = nil
    o.record_shadow = nil -- 上次bgsave时的值(单行模式用)
    o.delete_flag = {} -- 需要被删除的记录 
    o.update_flag = {} -- 需要被完整更新的记录
    o.dirty_flag = {}  -- 需要被部分更新的记录
    o.dirty_fields = {} -- 部分更新的字段

    o.loaded = false -- 数据是否已加载

    setmetatable(o, M)
    M.__index = M
    return o
end

--加载数据单表
function M:load_single(tablename, key, keyname, skn, other_fields, rkey)
    if not(tablename and key and keyname and other_fields and next(other_fields) and rkey) then
        print("########", tablename, key, keyname, other_fields, rkey)
        error_args("load_single", tablename, key, keyname, other_fields, rkey)
    end
    local ex = get_expire_time(tablename)
    local res = {}
    local result = g.callRedis("hmget", rkey, __ST, table.unpack(other_fields))
    if result and next(result) then
        local st = tonumber(result[1])
        if not st then
            g.log:warn("cache invalid, rkey=%s result=%s", rkey, tostring(result))
            g.callRedis("del", rkey)
        else
            local now = g.time()
            if need_re_expire(st, ex, now, tablename) then
                if g.callRedis("expire", rkey, ex) == "1" then
                    g.callRedis("hset", rkey, __ST, now)
                end
            end
            for i = 1, #other_fields do
                res[other_fields[i]] = result[i+1]
            end
            return res
        end
    end
    -- 从DB加载
    local sql = string.format("select %s,%s from %s where %s=%s", keyname,
            table.concat(other_fields, ","), tablename, keyname, quote(key))
    local dbres = execute_sql(sql)
    res = dbres[1] or res
    -- 更新到redis
    g.callRedis("hmset", rkey, __ST, g.time(), table.unpack(dict2kvlist(res)))
    g.callRedis("expire", rkey, ex)

    return res
end

function M:update_single(tablename, key, keyname, skn, rkey, change)
    if not (tablename and key and keyname and rkey and change and next(change)) then
        error_args("update_single", tablename, key, keyname, rkey, change)
    end


    local ok = g.callRedis("hmset", rkey, table.unpack(dict2kvlist(change)))
    if ok ~= "OK" then
        error_args("update_single update redis failed", tablename, key, keyname, rkey, change)
    end

    -- 更新db
    local fields = {keyname}
    local values = {key}
    local updates = {}
    for k, v in pairs(change) do
        fields[#fields + 1] = k
        values[#values + 1] = quote(v)
        updates[#updates + 1] = string.format("%s=values(%s)", k, k)
    end

    local sql = string.format("insert into %s(%s) values(%s) on duplicate key update %s",
            tablename, table.concat(fields, ","), table.concat(values, ","),
            table.concat(updates, ","))
    execute_sql(sql)
    return true
end

function M:delete_single(tablename, keyname, key, rkey)
    if not (tablename and keyname and rkey and key) then
        error_args("delete_single", tablename, keyname, key, rkey)
    end

    local sql = string.format("delete from %s where %s in (%s)", tablename, keyname, quote(key))
    g.callRedis("del", rkey)
    execute_sql(sql)

    return true
end

function M:update_single_mysql(tablename, key, keyname, change)
    if not(tablename and key and keyname and change) then
        error_args("update_single_mysql", tablename, key, keyname, change)
    end
    if not (next(change)) then return end

    local fields = {keyname}
    local values = {key}
    local updates = {}
    for k, v in pairs(change) do
        fields[#fields + 1] = k
        values[#values + 1] = quote(v)
        updates[#updates + 1] = string.format("%s=values(%s)", k, k)
    end

    local sql = string.format("insert into %s(%s) values(%s) on duplicate key update %s",
            tablename, table.concat(fields, ","), table.concat(values, ","),
            table.concat(updates, ","))
    local ret = execute_sql(sql)

    if ret and ret.affected_rows > 0 then
        return ec.ok
    else
        g.log:err("update_single_mysql failed ret:%s, sql:%s", tostring(ret), sql)
        return
    end
end


function M:load_multi(tablename, fkn, fk, skn, rkey, other_fields)
    if not(tablename and fkn and fk and skn and other_fields and
            next(other_fields) and rkey) then
        error_args("load_multi", tablename, fkn, fk, skn, other_fields, rkey)
    end
    local ex = get_expire_time(tablename)
    local res = {}
    local result = g.callRedis("hgetall", rkey)
    -- 从redis 加载
    if result and next(result) then
        local st
        local all = {}
        local sk, k, v
        for i = 1, #result, 2 do
            if result[i] == __ST then
                st = result[i + 1]
            else
                sk, k = string.match(result[i], "(.+):(.+)")
                sk = _try_number(sk)
                if sk and k then
                    all[sk] = all[sk] or {}
                    all[sk][k] = result[i + 1]
                else
                    g.log:warn("load_multi from redis, unknwon col ", result[i], rkey)
                end
            end
        end
        if not st then
            g.log:warn("cache invalid, rkey=%s result=%s", rkey, tostring(result))
            --rdb:del(rkey)
            g.sendRedis("del", rkey)

        else
            local now = g.time()
            if need_re_expire(st, ex, now, tablename) then

                if g.callRedis("expire", rkey, ex) == "1" then
                --if rdb:expire(rkey, ex) == "1" then
                --    rdb:hset(rkey, __ST, now)
                    g.sendRedis("hset", rkey, __ST, now)
                end
            end

            for sk, row in pairs(all) do
                res[sk] = futil.get_specific_fields(row, other_fields)
            end
            return res
        end
    end

    -- 从DB加载
    local sql = string.format("select %s,%s from %s where %s=%s", skn,
            table.concat(other_fields, ","), tablename, fkn, quote(fk))

    local dbres = execute_sql(sql)
    local records = dbres

    local rmset = {rkey, __ST, g.time()}
    for _, rec in pairs(records) do
        res[rec[skn]] = futil.get_specific_fields(rec, other_fields)
        for _, field in pairs(other_fields) do
            rmset[#rmset + 1] = string.format("%s:%s", rec[skn], field)
            rmset[#rmset + 1] = rec[field]
        end
    end
    g.sendRedis("hmset", rmset)
    g.sendRedis("expire", rkey, ex)
    --rdb:hmset(rmset)
    --rdb:expire(rkey, ex)

    return res
end

function M:update_multi_mysql(tablename, fk, fkn, skn, change)
    local all = {}
    for sk, rec in pairs(change) do
        local fields = {}
        local values = {quote(fk), quote(sk)}
        local updates = {}
        for k, v in pairs(rec) do
            fields[#fields + 1] = k
            values[#values + 1] = quote(v)
            updates[#updates +1] = string.format("%s=values(%s)", k, k)
        end
        local fields_str = table.concat(fields, ",")
        --local updates_str = table.concat(updates, ",")
        local values_str = string.format("(%s)", table.concat(values, ","))
        all[fields_str] = all[fields_str] or {vals_arr = {}, updates = table.concat(updates, ",")}
        table.insert(all[fields_str]["vals_arr"], values_str)
    end

    for fs, item in pairs(all) do
        local sql = string.format("insert into %s(%s,%s,%s) values %s on duplicate key update %s",
                tablename, fkn, skn, fs, table.concat(item.vals_arr, ","), item.updates)
        execute_sql(sql)
        --g.log:info("44444444444444 update_multi_mysql sql:%s", sql)
    end
end

function M:update_multi(tablename, fkn, fk, skn, rkey, change)
    if not(tablename and fk and fkn and skn and rkey and change) then
        error_args("update_multi", tablename, fk, fkn, skn, rkey, change)
    end
    if not (next(change)) then return true end

    local rmset = {rkey}
    for sk, rec in pairs(change) do
        for k, v in pairs(rec) do
            rmset[#rmset + 1] = string.format("%s:%s", sk, k)
            rmset[#rmset + 1] = v
        end
    end
    local ok = g.callRedis("hmset", rmset)
    if ok ~= "OK" then
        error_args("update_multi, update cache failed", tablename, fk, fkn, skn, rkey, change)
    end

    local all = {}
    for sk, rec in pairs(change) do
        local fields = {}
        local values = {quote(fk), quote(sk)}
        local updates = {}
        for k, v in pairs(rec) do
            fields[#fields + 1] = k
            values[#values + 1] = quote(v)
            updates[#updates +1] = string.format("%s=values(%s)", k, k)
        end
        local fields_str = table.concat(fields, ",")
        --local updates_str = table.concat(updates, ",")
        local values_str = string.format("(%s)", table.concat(values, ","))
        all[fields_str] = all[fields_str] or {vals_arr = {}, updates = table.concat(updates, ",")}
        table.insert(all[fields_str]["vals_arr"], values_str)
    end

    for fs, item in pairs(all) do
        local sql = string.format("insert into %s(%s,%s,%s) values %s on duplicate key update %s",
                tablename, fkn, skn, fs, table.concat(item.vals_arr, ","), item.updates)
        execute_sql(sql)
        --g.log:info("update mulit[sql]:%s", sql)
    end
    return true
end

function M:delete_multi(tablename, fkn, fk, skn, sks, rkey, fields)
    if not(tablename and fkn and skn and fields and rkey and fk and sks and next(sks)) then
        error_args("delete_multi", tablename, fkn, skn, fields, rkey, fk, sks)
    end
    --local rdb = _redis.common
    local del_fields = {}
    for _, sk in pairs(sks) do
        for _, f in pairs(fields) do
            del_fields[#del_fields + 1] = string.format("%s:%s", sk, f)
        end
    end
    g.callRedis("hdel", rkey, table.unpack(del_fields))
    --rdb:hdel(rkey, table.unpack(del_fields))

    for k, v in pairs(sks) do
        sks[k] = quote(v)
    end
    local sql = string.format("delete from %s where %s = %s and %s in(%s)",
            tablename, fkn, quote(fk), skn, table.concat(sks, ","))
    --g.log:debug("[md sql]: %s", sql)
    execute_sql(sql)
end


function M:load(first_key)
    if self.loaded then
        return true
    end
    self.rkey = g.const:getRedisKeyByTablename(self.table_name, first_key)
    --print("#####self.rkey", self.rkey, self.table_name)
    assert(self.rkey)
    self.first_key = first_key
    --load_single(tablename, fk, fkn, fields, rkey)
    local res = self:load_fname(self.table_name,
            self.first_key_name, first_key, self.second_key_name, self.rkey, 
            self.field_names)
    if not res then
        g.log:err("load %s faild, first_key = %s", self.table_name, first_key)
        return
    end
    local function _format(r)
        -- 填充默认值
        if not r then return end
        for k, v in pairs(self.fields) do
            if not r[k] and
                k ~= self.first_key_name and 
                k ~= self.second_key_name and
                v.Default then
                r[k] = v.Default
            end
        end
        for k, v in pairs(r) do 
            local dt = self.fields[k].Datatype
            if dt == DT.Integer then
                r[k] = math.tointeger(r[k])
            elseif dt == DT.Float or dt == DT.Double then
                r[k] = tonumber(r[k])
            elseif dt == DT.String and type(v) ~= "string" then
                r[k] = tostring(v)
            end
        end
        -- 然后调用自定义decode函数
        if self.decode then
            if not self.decode(r) then 
                g.log:ferr("lorm load, decode failed, table=%s, fk=%s, r=%s", self.table_name, first_key, tostring(r))
                return 
            end
        end
        return r
    end

    if self.second_key_name then
        self.records = res
        for k, r in pairs(self.records) do self.records[k] = _format(r) end
    else
        --if not next(res) then
        --    for _, k in pairs(self.field_names) do self.dirty_fields[k] = true end
        --end
        self.record = _format(res)
        self.record_shadow = table.clone(self.record)
    end
    if not(self.records or self.record) then return end
    self.loaded = true
    return true
end

-- 1)获取单行数据
-- 2)对于有second_key_name的根据第二个键值获取对应数据
function M:get(second_key)
    assert(self.loaded)
    if second_key then
        assert(self.second_key_name)
        return self.records[second_key]
    else 
        assert(not self.second_key_name)
        return self.record
    end
end

-- 返回所有数据
function M:get_all()
    if not self.loaded then return end

    if self.second_key_name then
        return self.records
    else
        return self.record
    end
end

-- 插入或更新整条记录,调用save时会更新到DB
-- |not_dirty|: 不让此条记录更新到DB (测试时可能会用到)
function M:update(second_key, val, not_dirty)
    if not self.loaded then return end
    if val then
        assert(self.second_key_name)
        self.records[second_key] = val
        if not not_dirty then  
            self.update_flag[second_key] = true 
            self.dirty_flag[second_key] = nil
            if self.delete_flag[second_key] then
                self.delete_flag[second_key] = nil
            end
        end
    else
        assert(not self.second_key_name) 
        self.record = second_key
        if not not_dirty then 
            for _, k in pairs(self.field_names) do self.dirty_fields[k] = true end
            if self.delete_flag[0] then self.delete_flag[0] = nil end
        end
    end
    return true
end

-- 指定哪些字段在调用save时会被更新到DB
function M:set_dirty(...)
    if self.second_key_name then -- 多行
        local len = select('#', ...) 
        if len < 2 then g.log:ferr("set_dirty failed, invalid args len") end

        local sk = ...
        local rec = self.records[sk]
        if not rec then 
            g.log:ferr("set_dirty failed, record not found, table=%s sk=%s", 
                    self.table_name, sk) 
            return
        end
        if self.update_flag[sk] then return end

        for _, k in pairs({select(2,...)}) do
            if self.fields[k] then self.dirty_fields[k] = true end
        end
        self.dirty_flag[sk] = true
    else --单行
        if not self.record then
            g.log:ferr("set_dirty failed, record not found, table=%s fk=%",
                    self.table_name, self.first_key)
            return
        end
        for _, k in pairs({...}) do
            if self.fields[k] then self.dirty_fields[k] = true end
        end
    end
end

function M:remove(second_key)
    if not self.loaded then return end
    if second_key then
        assert(self.second_key_name)
        self.records[second_key] = nil
        if self.update_flag[second_key] then 
            self.update_flag[second_key] = nil 
        end
        if self.dirty_flag[second_key] then
            self.dirty_flag[second_key] = nil
            if not next(self.dirty_flag) then self.dirty_fields = {} end
        end
        self.delete_flag[second_key] = true
    else
        assert(not self.second_key_name) 
        self.record = nil
        if next(self.dirty_fields) then
            self.dirty_fields = {}
        end
        self.delete_flag[0] = true
    end
end

function M:save()
    if not self.loaded then return end
    if self.second_key_name then -- 多行
        if next(self.update_flag) then
            local updates = {}
            for sk, _ in pairs(self.update_flag) do
                if self.records[sk] then 
                    updates[sk] = table.clone(self.records[sk])
                    if self.encode then updates[sk] = self.encode(updates[sk]) end
                end
            end
            if next(updates) then
                assert(self.update_fname)
                local old_update_flag = table.copy(self.update_flag)
                self.update_flag = {}
                local ok = self:update_fname(self.table_name,
                        self.first_key_name, self.first_key, self.second_key_name, 
                        self.rkey, updates)
                if not ok then
                    g.log:err("update failed, table_name = %s, first_key = %s", self.table_name, self.first_key)
                    for k, _ in pairs(old_update_flag) do self.update_flag[k] = true end
                end
            end
        end

        if next(self.dirty_flag) then
            assert(next(self.dirty_fields))
            local updates = {}
            for sk, _ in pairs(self.dirty_flag) do
                local update = {}
                local rec = self.records[sk]
                for k, _ in pairs(self.dirty_fields) do
                    if type(rec[k]) == "table" then
                        update[k] = table.clone(rec[k])
                    else
                        update[k] = rec[k]
                    end
                end
                updates[sk] = update
                if self.encode then updates[sk] = self.encode(updates[sk]) end
            end

            if next(updates) then
                local old_dirty_flag = table.copy(self.dirty_flag)
                self.dirty_flag = {}
                local ok = self:update_fname(self.table_name,
                        self.first_key_name, self.first_key, self.second_key_name, 
                        self.rkey, updates)
                if not ok then
                    g.log:ferr("update failed, table_name = %s, first_key = %s", self.table_name, self.first_key)
                    for k, _ in pairs(old_dirty_flag) do self.dirty_flag[k] = true end
                end
            end
        end

        if next(self.delete_flag) then
            local ok = self:delete_fname(self.table_name,
					self.first_key_name, self.first_key, self.second_key_name, 
                    table.to_array(self.delete_flag), self.rkey, self.field_names)
            if not ok then
                g.log:ferr("delete failed, table_name = %s, first_key = %s, sks=%s", 
                        self.table_name, self.first_key, tostring(self.delete_flag))
            else
                self.delete_flag = {}
            end
        end
    else -- 单行
        local update = {}
        for k, v in pairs(self.record) do
            if self.dirty_fields[k] then
                if type(v) == "table" then
                    update[k] = table.clone(v)
                else
                    update[k] = v
                end
            elseif not (type(v) == "table" or v == self.record_shadow[k]) then
                update[k] = v
            end
        end

        if self.encode then update = self.encode(update) end
        if not (update and next(update)) then return end

        local old_dirty_fields
        if next(self.dirty_fields) then 
            old_dirty_fields = table.copy(self.dirty_fields)
            self.dirty_fields = {}
        end
        local ok = self:update_fname(self.table_name,
                self.first_key_name, self.first_key, self.second_key_name, 
                self.rkey, update)
        if not ok then
            g.log:ferr("update failed, table_name = %s, first_key = %s", 
                    self.table_name, self.first_key)

            if old_dirty_fields then
                for k, _ in pairs(old_dirty_fields) do
                    self.dirty_fields[k] = true
                end
            end
        else
            for k, v in pairs(update) do
                if type(v) ~= "table" then self.record_shadow[k] = v end
            end
        end

        if self.delete_flag[0] then
            assert(self.delete_fname)
            local ok = g.db:delete_fname(self.table_name,
                    self.first_key_name, self.first_key, self.rkey)
            if not ok then
                g.log:ferr("delete failed, table_name = %s, first_key = %s", self.table_name, self.first_key)
            else
                self.delete_flag = {}
            end
        end
    end
end

function M:is_loaded()
    return self.loaded
end

return M
